﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using JiaBao.Core.ComponentModel;
using JiaBao.Core.Plugins;



[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]//ASP.Net 标记会在程序启动前执行
namespace JiaBao.Core.Plugins
{
    /// <summary>
    /// 插件管理器
    /// </summary>
    public class PluginManager
    {
        #region 常量

        /// <summary>
        /// 记录文件路径
        /// </summary>
        private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt";

        /// <summary>
        /// 插件目录
        /// </summary>
        private const string PluginsPath = "~/Plugins";

        /// <summary>
        /// 插件复制目录
        /// </summary>
        private const string ShadowCopyPath = "~/Plugins/bin";

        #endregion

        #region 字段

        /// <summary>
        /// 资源访问锁
        /// </summary>
        private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();

        /// <summary>
        /// ASP.NET非最高权限时运行库
        /// </summary>
        private static DirectoryInfo _shadowCopyFolder;

        /// <summary>
        /// 是否清除bin文件
        /// </summary>
        private static bool _clearShadowDirectoryOnStartup;

        #endregion

        #region 属性
        /// <summary>
        /// 已加载插件
        /// </summary>
        public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; }

        /// <summary>
        /// 不兼容插件名称
        /// </summary>
        public static IEnumerable<string> IncompatiblePlugins { get; set; }
        #endregion


        /// <summary>
        /// ASP.NET环境下 程序启动前执行
        /// </summary>
        public static void Initialize()
        {
            using (new WriteLockDisposable(Locker))
            {
                //获取插件及bin文件夹路径
                var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath));
                _shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath));

                //已加载的插件
                var referencedPlugins = new List<PluginDescriptor>();

                //不兼容插件
                var incompatiblePlugins = new List<string>();

                //获取配置节点中 ClearPluginsShadowDirectoryOnStartup 是否清除插件bin
                _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
                   Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]);

                try
                {
                    //解析已安装插件 目录文件
                    var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());

                    Debug.WriteLine("确保插件及Bin文件夹存在");
                    Directory.CreateDirectory(pluginFolder.FullName);
                    Directory.CreateDirectory(_shadowCopyFolder.FullName);


                    //获取bin文件夹中所有文件
                    var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
                    //是否清除bin文件
                    if (_clearShadowDirectoryOnStartup)
                    {
                        //clear out shadow copied plugins
                        foreach (var f in binFiles)
                        {
                            Debug.WriteLine("删除 : " + f.Name);
                            try
                            {
                                File.Delete(f.FullName);
                            }
                            catch (Exception exc)
                            {
                                Debug.WriteLine("删除文件[ " + f.Name + "]错误:" + exc);
                            }
                        }
                    }


                    //获取遍历所有说明文件
                    foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder))
                    {
                        //文件
                        var descriptionFile = dfd.Key;
                        //说明类
                        var pluginDescriptor = dfd.Value;

                        //检测框架是否兼容插件
                        if (!pluginDescriptor.SupportedVersions.Contains(JiaBaoVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
                        {
                            //不兼容
                            incompatiblePlugins.Add(pluginDescriptor.SystemName);
                            continue;
                        }

                        //SystemName
                        if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName))
                            throw new Exception(string.Format("插件 : '{0}' SystemName为空", descriptionFile.FullName));
                        if (referencedPlugins.Contains(pluginDescriptor))
                            throw new Exception(string.Format("插件 : '{0}' 已加载", pluginDescriptor.SystemName));

                        //检测插件是否已安装并保存到说明
                        pluginDescriptor.Installed = installedPluginSystemNames
                            .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null;

                        //获取插件复制并引用
                        try
                        {
                            if (descriptionFile.Directory == null)
                                throw new Exception(string.Format("插件 : '{0}' 目录错误", descriptionFile.Name));
                            //获取插件包下所有dll文件
                            var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
                                .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName))//
                                .Where(x => IsPackagePluginFolder(x.Directory))
                                .ToList();

                            //追加EXE文件
                            pluginFiles.AddRange(
                                            descriptionFile.Directory.GetFiles("*.exe", SearchOption.AllDirectories)
                                            .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName))//
                                            .Where(x => IsPackagePluginFolder(x.Directory))
                                            .ToList());

                            //获取插件主Dll文件
                            var mainPluginFile = pluginFiles
                                .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase));

                            //赋值说明文件的原始DLL属性
                            pluginDescriptor.OriginalAssemblyFile = mainPluginFile;

                            //复制主DLL到ASP.NET运行库
                            //并加载程序集
                            pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile);

                            //加载所有其他引用的程序集
                            foreach (var plugin in pluginFiles
                                                    .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))//排除主DLL
                                                    .Where(x => !IsAlreadyLoaded(x)))//排除程序已加载程序集文件
                                PerformFileDeploy(plugin);//复制主DLL到ASP.NET运行库  并加载程序集

                            //赋值说明文件 插件入口类 (每个程序集只能有一个)
                            //实现IPlugin 是Class 非抽象类
                            foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes())
                                if (typeof(IPlugin).IsAssignableFrom(t))
                                    if (!t.IsInterface)
                                        if (t.IsClass && !t.IsAbstract)
                                        {
                                            pluginDescriptor.PluginType = t;
                                            break;
                                        }
                            //加入到已加载集合
                            referencedPlugins.Add(pluginDescriptor);
                        }
                        catch (ReflectionTypeLoadException ex)
                        {
                            //无法加载错误

                            var msg = string.Format("插件 '{0}'. ", pluginDescriptor.FriendlyName);
                            foreach (var e in ex.LoaderExceptions)
                                msg += e.Message + Environment.NewLine;

                            var fail = new Exception(msg, ex);
                            throw fail;
                        }
                        catch (Exception ex)
                        {
                            //其他错误
                            var msg = string.Format("插件 '{0}'. {1}", pluginDescriptor.FriendlyName, ex.Message);

                            var fail = new Exception(msg, ex);
                            throw fail;
                        }
                    }
                }
                catch (Exception ex)
                {
                    var msg = string.Empty;
                    for (var e = ex; e != null; e = e.InnerException)
                        msg += e.Message + Environment.NewLine;

                    var fail = new Exception(msg, ex);
                    throw fail;
                }
                //添加执行结果到对应属性
                ReferencedPlugins = referencedPlugins;
                IncompatiblePlugins = incompatiblePlugins;
            }
        }

        /// <summary>
        /// 加载插件
        /// </summary>
        /// <param name="systemName"></param>
        public static void MarkPluginAsInstalled(string systemName)
        {
            if (string.IsNullOrEmpty(systemName))
                throw new ArgumentNullException("systemName");

            //获取记录文件路径
            var filePath = GetInstalledPluginsFilePath();

            if (!File.Exists(filePath))
                using (File.Create(filePath))
                {
                    //保证目录文件存在            
                }

            //读取目录文件
            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(filePath);

            //判断目录文件是否包含 systemName
            bool alreadyMarkedAsInstalled = installedPluginSystemNames.FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;

            if (!alreadyMarkedAsInstalled)
                installedPluginSystemNames.Add(systemName);

            //保存目录文件
            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);

        }


        /// <summary>
        /// 卸载插件
        /// </summary>
        /// <param name="systemName"></param>
        public static void MarkPluginAsUninstalled(string systemName)
        {
            if (string.IsNullOrEmpty(systemName))
                throw new ArgumentNullException("systemName");

            var filePath = GetInstalledPluginsFilePath();
            if (!File.Exists(filePath))
                using (File.Create(filePath)) { }

            var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(filePath);
            bool alreadyMarkedAsInstalled = installedPluginSystemNames.FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;

            if (alreadyMarkedAsInstalled)
                installedPluginSystemNames.Remove(systemName);

            PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames, filePath);
        }

        /// <summary>
        /// 卸载所有插件
        /// </summary>
        public static void MarkAllPluginsAsUninstalled()
        {
            var filePath = GetInstalledPluginsFilePath();
            if (File.Exists(filePath))
                File.Delete(filePath);
        }


        #region 内部方法

        /// <summary>
        /// 获取目录文件路径
        /// </summary>
        /// <returns></returns>
        private static string GetInstalledPluginsFilePath()
        {
            return CommonHelper.MapPath(InstalledPluginsFilePath);
        }

        /// <summary>
        /// 获取所有插件说明文件及说明
        /// </summary>
        /// <param name="pluginFolder">插件文件夹</param>
        /// <returns></returns>
        private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder)
        {
            if (pluginFolder == null)
                throw new ArgumentNullException("pluginFolder");

            var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>();

            //遍历 插件文件夹下 所有Description.txt 文件
            foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories))
            {
                //Description.txt 必须在插件根目录
                if (!IsPackagePluginFolder(descriptionFile.Directory))
                    continue;

                var pluginDescriptor = PluginFileParser.ParsePluginDescriptorFile(descriptionFile.FullName);
                result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor));
            }

            //排序返回
            result.Sort((firstPair, nextPair) => firstPair.Value.Order.CompareTo(nextPair.Value.Order));
            return result;
        }

        /// <summary>
        /// 是否为插件文件夹
        /// </summary>
        /// <param name="folder"></param>
        /// <returns></returns>
        private static bool IsPackagePluginFolder(DirectoryInfo folder)
        {
            if (folder == null) return false;
            if (folder.Parent == null) return false;
            if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false;
            return true;
        }


        /// <summary>
        /// 验证指定文件程序集是否已加载
        /// </summary>
        /// <param name="fileInfo">待验证</param>
        /// <returns></returns>
        private static bool IsAlreadyLoaded(FileInfo fileInfo)
        {
            try
            {
                //
                string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
                if (fileNameWithoutExt == null)
                    throw new Exception(string.Format("无法获取{0}的扩展名", fileInfo.Name));

                //遍历已加载程序集
                foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
                {
                    string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault();
                    if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
                        return true;
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine("无法验证程序集是否已加载", ex);
            }
            return false;
        }

        #region 根据 Asp.net 信任等级 复制主DLL文件到运行库

        /// <summary>
        /// 复制文件到程序运行库 并加载程序集(ASP.NET 程序)
        /// 根据不同信任级别复制到不同地方
        /// </summary>
        /// <param name="plug">需要复制的文件</param>
        /// <returns></returns>
        private static Assembly PerformFileDeploy(FileInfo plug)
        {
            if (plug.Directory == null || plug.Directory.Parent == null)
                throw new InvalidOperationException("插件'" + plug.Name + "'的文件夹层次不符合规定");

            FileInfo shadowCopiedPlug;
            Assembly shadowCopiedAssembly;
            //web项目判断
            if (!HostingEnvironment.IsHosted)
            {
                #region 非WEB项目   
                var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
                shadowCopiedPlug = InitializeMediumTrust(plug, new DirectoryInfo(baseDirectory));
                #endregion

                //加载插件程序集
                shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
                AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
            }
            else
            {
                #region Web项目

                if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
                {
                    //非最高权限
                    var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
                    //复制文件到指定路径
                    shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
                }
                else
                {
                    //最高权限
                    var directory = AppDomain.CurrentDomain.DynamicDirectory;
                    Debug.WriteLine(plug.FullName + " --> " + directory);
                    //复制文件到指定路径
                    shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
                }
                #endregion

                //加载插件程序集
                shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));
                //添加程序集到ASP.NET应用
                BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
            }



            return shadowCopiedAssembly;
        }

        /// <summary>
        /// 复制到自定义bin文件夹(ASP.NET Medium权限)
        /// </summary>
        /// <param name="plug">需要复制的文件</param>
        /// <param name="shadowCopyPlugFolder">目标文件夹</param>
        /// <returns></returns>
        private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
        {
            var shouldCopy = true;

            //目标
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));

            //文件存在
            if (shadowCopiedPlug.Exists)
            {
                //比较连个创建日期
                var areFilesIdentical = shadowCopiedPlug.LastWriteTimeUtc.Ticks >= plug.LastWriteTimeUtc.Ticks;



                if (areFilesIdentical)
                {
                    //不需要复制
                    Debug.WriteLine(shadowCopiedPlug.Name + " 插件不需要复制");
                    shouldCopy = false;
                }
                else
                {
                    //删除现有复制
                    Debug.WriteLine(shadowCopiedPlug.Name + " 新插件需要复制");
                    File.Delete(shadowCopiedPlug.FullName);
                }
            }

            //复制
            if (shouldCopy)
            {
                try
                {
                    File.Copy(plug.FullName, shadowCopiedPlug.FullName);
                }
                catch (IOException)
                {
                    //有时重命名可以解除锁定
                    Debug.WriteLine(shadowCopiedPlug.FullName + " 被锁定,尝试重命名操作再复制");
                    try
                    {
                        var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                        File.Move(shadowCopiedPlug.FullName, oldFile);
                    }
                    catch (IOException ex)
                    {

                        throw new IOException(shadowCopiedPlug.FullName + " 重命名失败,无法复制插件", ex);
                    }

                    File.Copy(plug.FullName, shadowCopiedPlug.FullName);
                }

            }

            return shadowCopiedPlug;
        }

        /// <summary>
        /// 复制到标准的动态文件夹(ASP.NET最高权限)
        /// </summary>
        /// <param name="plug">需要复制的文件</param>
        /// <param name="shadowCopyPlugFolder">目标文件夹</param>
        /// <returns></returns>
        private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
        {
            //目标
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));

            try
            {
                File.Copy(plug.FullName, shadowCopiedPlug.FullName);
            }
            catch (IOException)
            {

                Debug.WriteLine(shadowCopiedPlug.FullName + " 被锁定,尝试重命名操作再复制");
                try
                {
                    var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                    File.Move(shadowCopiedPlug.FullName, oldFile);
                }
                catch (IOException ex)
                {

                    throw new IOException(shadowCopiedPlug.FullName + " 重命名失败,无法复制插件", ex);
                }

                File.Copy(plug.FullName, shadowCopiedPlug.FullName);
            }
            return shadowCopiedPlug;
        }

        #endregion

        #endregion
    }
}
